package labox.innovation.gameserver.scripting;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.InvalidClassException;
import java.io.LineNumberReader;
import java.io.ObjectInputStream;
import java.io.UnsupportedEncodingException;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javax.script.Compilable;
import javax.script.CompiledScript;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import javax.script.SimpleScriptContext;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javolution.util.FastMap;
import labox.innovation.config.Config;

/**
 * Caches script engines and provides funcionality for executing and managing scripts.<BR>
 * 缓存的脚本引擎，并提供执行和管理脚本功能
 * 
 * @author KenM
 */
public final class L2ScriptEngineManager {

	private static final Logger _log = LoggerFactory.getLogger(L2ScriptEngineManager.class.getName());

	public final static File SCRIPT_FOLDER = new File(Config.DATAPACK_ROOT, "/data/scripts");

	private final Map<String, ScriptEngine> _nameEngines = new FastMap<String, ScriptEngine>();

	private final Map<String, ScriptEngine> _extEngines = new FastMap<String, ScriptEngine>();

	private final List<ScriptManager<?>> _scriptManagers = new LinkedList<ScriptManager<?>>();

	private final CompiledScriptCache _cache;

	private File _currentLoadingScript;

	// Configs
	// TODO move to config file
	/**
	 * 通知（日志）加载的脚本。<BR>
	 * 只适用于从文件中执行的脚本。<BR>
	 */
	private final boolean VERBOSE_LOADING = false;

	/**
	 * 编译执行脚本(false:解释执行)
	 */
	private final boolean ATTEMPT_COMPILATION = true;

	/**
	 * 使用编译脚本缓存(暂不使用)
	 */
	private final boolean USE_COMPILED_CACHE = false;

	/**
	 * 清楚错误日志 <BR>
	 * 只适用于从文件中执行的脚本。
	 */
	private final boolean PURGE_ERROR_LOG = true;

	public static L2ScriptEngineManager getInstance() {
		return SingletonHolder._instance;
	}

	private L2ScriptEngineManager() {
		ScriptEngineManager scriptEngineManager = new ScriptEngineManager();

		List<ScriptEngineFactory> factories = scriptEngineManager.getEngineFactories();

		_cache = loadCompiledScriptCache();

		_log.info("Initializing Script Engine Manager", Config.SERVER_ID, "脚本引擎");

		for (ScriptEngineFactory factory : factories) {

			try {
				ScriptEngine engine = factory.getScriptEngine();

				boolean reg = false;
				for (String name : factory.getNames()) {

					ScriptEngine existentEngine = _nameEngines.get(name);

					if (existentEngine != null) {
						double engineVer = Double.parseDouble(factory.getEngineVersion());
						double existentEngVer = Double.parseDouble(existentEngine.getFactory().getEngineVersion());

						if (engineVer <= existentEngVer) {
							continue;
						}
					}

					reg = true;
					_nameEngines.put(name, engine);
				}

				if (reg) {
					_log.info("Script Engine: " + factory.getEngineName() + " " + factory.getEngineVersion() + " - Language: " + factory.getLanguageName() + " - Language Version: " + factory.getLanguageVersion(),Config.SERVER_ID,"脚本引擎");
				}

				for (String ext : factory.getExtensions()) {

					if (!ext.equals("java") || factory.getLanguageName().equals("java")) {
						_extEngines.put(ext, engine);
					}
				}
			} catch (Exception e) {
				_log.error("Failed initializing factory. ", Config.SERVER_ID, e);
				e.printStackTrace();
			}
		}

		//		{
		//			
		//			_nameEngines.put("jython", new JythonScriptEngineFactory().getScriptEngine());
		//		}

		// preConfigure();
	}

	// private void preConfigure() {
	// java class path

	// Jython sys.path 添加一个目录名称到 sys.path 里
	// String dataPackDirForwardSlashes =
	// SCRIPT_FOLDER.getPath().replaceAll("\\\\", "/");
	// String configScript = "import sys;sys.path.insert(0,'" +
	// dataPackDirForwardSlashes + "');";
	// try {
	// eval("jython", configScript);
	// } catch (ScriptException e) {
	// _log.severe("Failed preconfiguring jython: " + e.getMessage());
	// }
	// }

	private ScriptEngine getEngineByName(String name) {
		return _nameEngines.get(name);
	}

	private ScriptEngine getEngineByExtension(String ext) {
		return _extEngines.get(ext);
	}

	public void executeScriptList(File list) throws IOException {
		if (list.isFile()) {
			LineNumberReader lnr = new LineNumberReader(new InputStreamReader(new FileInputStream(list), "UTF-8"));
			String line;
			File file;

			while ((line = lnr.readLine()) != null) {
				String[] parts = line.trim().split("#");

				if (parts.length > 0 && !parts[0].startsWith("#") && parts[0].length() > 0) {
					line = parts[0];

					if (line.endsWith("/**")) {
						line = line.substring(0, line.length() - 3);
					} else if (line.endsWith("/*")) {
						line = line.substring(0, line.length() - 2);
					}

					file = new File(SCRIPT_FOLDER, line);

					if (file.isDirectory() && parts[0].endsWith("/**")) {
						this.executeAllScriptsInDirectory(file, true, 32);
					} else if (file.isDirectory() && parts[0].endsWith("/*")) {
						this.executeAllScriptsInDirectory(file);
					} else if (file.isFile()) {
						try {
							executeScript(file);
						} catch (ScriptException e) {
							reportScriptFileError(file, e);
							if (Config.DEVELOPER) {
								e.printStackTrace();
							}
						}
					} else {
						Object[] temp = { file.getCanonicalPath(), list.getName(), lnr.getLineNumber() };
						_log.warn("Failed loading: ( {" + temp[0] + "} ) @ :{" + temp[1] + "} {" + temp[2] + "} - Reason: doesnt exists or is not a file.", Config.SERVER_ID, "脚本引擎");
					}
				}
			}
			lnr.close();
		} else {
			throw new IllegalArgumentException("Argument must be an file containing a list of scripts to be loaded");
		}
	}

	public void executeAllScriptsInDirectory(File dir) {
		this.executeAllScriptsInDirectory(dir, false, 0);
	}

	/**
	 * 
	 * @param dir
	 *            文件/目录
	 * @param recurseDown
	 *            是否向下递归遍历
	 * @param maxDepth
	 *            最大文件目录遍历深度<br>
	 *            2010-3-8
	 * @author lyh
	 */
	public void executeAllScriptsInDirectory(File dir, boolean recurseDown, int maxDepth) {
		this.executeAllScriptsInDirectory(dir, recurseDown, maxDepth, 0);
	}

	/**
	 * 
	 * @param dir
	 *            文件/目录
	 * @param recurseDown
	 *            是否向下递归遍历
	 * @param maxDepth
	 *            最大文件目录遍历深度
	 * @param currentDepth
	 *            文件目录当前深度<br>
	 *            2010-3-8
	 * @author lyh
	 * @throws UnsupportedEncodingException 
	 */
	private void executeAllScriptsInDirectory(File dir, boolean recurseDown, int maxDepth, int currentDepth) {
		if (dir.isDirectory()) {
			for (File file : dir.listFiles()) {
				if (file.isDirectory() && recurseDown && maxDepth > currentDepth) {
					if (VERBOSE_LOADING) {
						_log.info("Entering folder: " + file.getName(), Config.SERVER_ID, "脚本引擎");
					}
					this.executeAllScriptsInDirectory(file, recurseDown, maxDepth, currentDepth + 1);
				} else if (file.isFile()) {
					try {
						String name = file.getName();
						int lastIndex = name.lastIndexOf('.');
						String extension;
						if (lastIndex != -1) {
							extension = name.substring(lastIndex + 1);
							ScriptEngine engine = this.getEngineByExtension(extension);
							if (engine != null) {
								this.executeScript(engine, file);
							}
						}
					} catch (FileNotFoundException e) {
						// should never happen
						e.printStackTrace();
					} catch (ScriptException e) {
						this.reportScriptFileError(file, e);
						//e.printStackTrace();
					} catch(UnsupportedEncodingException e) {
						
					}
				}
			}
		} else {
			throw new IllegalArgumentException("The argument directory either doesnt exists or is not an directory.");
		}
	}

	public CompiledScriptCache getCompiledScriptCache() throws IOException {
		return _cache;
	}

	public CompiledScriptCache loadCompiledScriptCache() {
		if (USE_COMPILED_CACHE) {
			File file = new File(SCRIPT_FOLDER, "CompiledScripts.cache");
			if (file.isFile()) {
				ObjectInputStream ois = null;
				try {
					ois = new ObjectInputStream(new FileInputStream(file));
					CompiledScriptCache cache = (CompiledScriptCache) ois.readObject();
					return cache;
				} catch (InvalidClassException e) {
					_log.error("Failed loading Compiled Scripts Cache, invalid class (Possibly outdated).{}", Config.SERVER_ID, e);
				} catch (IOException e) {
					_log.error("Failed loading Compiled Scripts Cache from file.{}", Config.SERVER_ID, e);
				} catch (ClassNotFoundException e) {
					_log.error("Failed loading Compiled Scripts Cache, class not found.{}", Config.SERVER_ID, e);
				} finally {
					try {
						ois.close();
					} catch (Exception e) {
					}
				}
				return new CompiledScriptCache();
			} else {
				return new CompiledScriptCache();
			}
		}
		return null;
	}

	public void executeScript(File file) throws ScriptException, FileNotFoundException, UnsupportedEncodingException {
		String name = file.getName();
		int lastIndex = name.lastIndexOf('.');
		String extension;
		if (lastIndex != -1) {
			extension = name.substring(lastIndex + 1);
		} else {
			throw new ScriptException("Script file (" + name + ") doesnt has an extension that identifies the ScriptEngine to be used.");
		}

		ScriptEngine engine = this.getEngineByExtension(extension);
		if (engine == null) {
			throw new ScriptException("No engine registered for extension (" + extension + ")");
		} else {
			executeScript(engine, file);
		}
	}

	public void executeScript(String engineName, File file) throws FileNotFoundException, ScriptException, UnsupportedEncodingException {
		ScriptEngine engine = this.getEngineByName(engineName);
		if (engine == null) {
			throw new ScriptException("No engine registered with name (" + engineName + ")");
		} else {
			executeScript(engine, file);
		}
	}

	public void executeScript(ScriptEngine engine, File file) throws FileNotFoundException, ScriptException, UnsupportedEncodingException {
		BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file),"GBK"));  //-- GBK编码

		if (VERBOSE_LOADING) {
			_log.info("Loading Script: " + file.getAbsolutePath(), Config.SERVER_ID, "脚本引擎");
		}

		if (PURGE_ERROR_LOG) {
			String name = file.getAbsolutePath() + ".error.log";
			File errorLog = new File(name);
			if (errorLog.isFile()) {
				errorLog.delete();
			}
		}
		
		

		if (engine instanceof Compilable && ATTEMPT_COMPILATION) {
			ScriptContext context = new SimpleScriptContext();
			context.setAttribute("mainClass", getClassForFile(file).replace('/', '.').replace('\\', '.'), ScriptContext.ENGINE_SCOPE);
			context.setAttribute(ScriptEngine.FILENAME, file.getName(), ScriptContext.ENGINE_SCOPE);
			context.setAttribute("classpath", SCRIPT_FOLDER.getAbsolutePath(), ScriptContext.ENGINE_SCOPE);
			context.setAttribute("sourcepath", SCRIPT_FOLDER.getAbsolutePath(), ScriptContext.ENGINE_SCOPE);
			
			setCurrentLoadingScript(file);
			ScriptContext ctx = engine.getContext();
			try {
				engine.setContext(context);
				
				if (USE_COMPILED_CACHE) {
					CompiledScript cs = _cache.loadCompiledScript(engine, file);
					cs.eval(context);
				} else {
					Compilable eng = (Compilable) engine;
					CompiledScript cs = eng.compile(reader);
					cs.eval(context);
				}
			} finally {
				engine.setContext(ctx);
				this.setCurrentLoadingScript(null);
				context.removeAttribute(ScriptEngine.FILENAME, ScriptContext.ENGINE_SCOPE);
				context.removeAttribute("mainClass", ScriptContext.ENGINE_SCOPE);
			}
		} else {
			ScriptContext context = new SimpleScriptContext();
			context.setAttribute("mainClass", getClassForFile(file).replace('/', '.').replace('\\', '.'), ScriptContext.ENGINE_SCOPE);
			context.setAttribute(ScriptEngine.FILENAME, file.getName(), ScriptContext.ENGINE_SCOPE);
			context.setAttribute("classpath", SCRIPT_FOLDER.getAbsolutePath(), ScriptContext.ENGINE_SCOPE);
			context.setAttribute("sourcepath", SCRIPT_FOLDER.getAbsolutePath(), ScriptContext.ENGINE_SCOPE);
			
			this.setCurrentLoadingScript(file);
			try {
				engine.eval(reader, context);
			} finally {
				this.setCurrentLoadingScript(null);
				engine.getContext().removeAttribute(ScriptEngine.FILENAME, ScriptContext.ENGINE_SCOPE);
				engine.getContext().removeAttribute("mainClass", ScriptContext.ENGINE_SCOPE);
			}

		}
	}

	public static String getClassForFile(File script) {
		String path = script.getAbsolutePath();
		String scpPath = SCRIPT_FOLDER.getAbsolutePath();
		if (path.startsWith(scpPath)) {
			int idx = path.lastIndexOf('.');
			return path.substring(scpPath.length() + 1, idx);
		}
		return null;
	}

	public ScriptContext getScriptContext(ScriptEngine engine) {
		return engine.getContext();
	}

	public ScriptContext getScriptContext(String engineName) {
		ScriptEngine engine = this.getEngineByName(engineName);
		if (engine == null) {
			throw new IllegalStateException("No engine registered with name (" + engineName + ")");
		} else {
			return this.getScriptContext(engine);
		}
	}

	public Object eval(ScriptEngine engine, String script, ScriptContext context) throws ScriptException {
		if (engine instanceof Compilable && ATTEMPT_COMPILATION) {
			Compilable eng = (Compilable) engine;
			CompiledScript cs = eng.compile(script);
			return context != null ? cs.eval(context) : cs.eval();
		} else {
			return context != null ? engine.eval(script, context) : engine.eval(script);
		}
	}

	public Object eval(String engineName, String script) throws ScriptException {
		return eval(engineName, script, null);
	}

	public Object eval(String engineName, String script, ScriptContext context) throws ScriptException {
		ScriptEngine engine = getEngineByName(engineName);
		if (engine == null) {
			throw new ScriptException("No engine registered with name (" + engineName + ")");
		} else {
			return eval(engine, script, context);
		}
	}

	public Object eval(ScriptEngine engine, String script) throws ScriptException {
		return this.eval(engine, script, null);
	}

	public void reportScriptFileError(File script, ScriptException e) {
		String dir = script.getParent();
		String name = script.getName() + ".error.log";
		if (dir != null) {
			File file = new File(dir + "/" + name);
			FileOutputStream fos = null;
			try {
				if (!file.exists()) {
					file.createNewFile();
				}

				fos = new FileOutputStream(file);
				String errorHeader = "Error on: " + file.getCanonicalPath() + "\r\nLine: " + e.getLineNumber() + " - Column: " + e.getColumnNumber() + "\r\n\r\n";
				fos.write(errorHeader.getBytes());
				fos.write(e.getMessage().getBytes());
				_log.warn("Failed executing script: {" + script.getAbsolutePath() +"} . See {" + file.getName() + "} for details.", Config.SERVER_ID, "脚本引擎");
			} catch (IOException ioe) {
				_log.error("Failed executing script: " + script.getAbsolutePath() + "\r\n" + e.getMessage() + "Additionally failed when trying to write an error report on script directory. Reason: " + ioe.getMessage(), Config.SERVER_ID, ioe);
				ioe.printStackTrace();
			} finally {
				try {
					fos.close();
				} catch (Exception e1) {
				}
			}
		} else {
			_log.warn("Failed executing script: {" + script.getAbsolutePath() + "} \r\n {} Additionally failed when trying to write an error report on script directory.", Config.SERVER_ID, e);
		}
	}

	public void registerScriptManager(ScriptManager<?> manager) {
		_scriptManagers.add(manager);
	}

	public void removeScriptManager(ScriptManager<?> manager) {
		_scriptManagers.remove(manager);
	}

	public List<ScriptManager<?>> getScriptManagers() {
		return _scriptManagers;

	}

	/**
	 * @param currentLoadingScript
	 *            The currentLoadingScript to set.
	 */
	protected void setCurrentLoadingScript(File currentLoadingScript) {
		_currentLoadingScript = currentLoadingScript;
	}

	/**
	 * @return Returns the currentLoadingScript.
	 */
	protected File getCurrentLoadingScript() {
		return _currentLoadingScript;
	}

	@SuppressWarnings("synthetic-access")
	private static class SingletonHolder {
		protected static final L2ScriptEngineManager _instance = new L2ScriptEngineManager();
	}
}
